/*
 *  Copyright 2012 Alexey Andreev.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package ru.justplay.chess.actors;

import java.io.Serializable;
import java.util.*;

/**
 *
 * @author Alexey Andreev
 */
public class DefaultBoard implements Board, Serializable {
    private static final long serialVersionUID = 1666100792938012078L;
    private List<PlayerObserver> observers = new ArrayList<PlayerObserver>();
    private PieceState[][] cells = new PieceState[8][];
    private PlayerImpl whitePlayer;
    private PlayerImpl blackPlayer;
    private Map<PieceColor, PlayerImpl> players = new HashMap<PieceColor, PlayerImpl>();
    private List<Move> moves = new ArrayList<Move>();

    private static class MoveResult {
        public Runnable cancel;
        public MoveRejectionReason rejectionReason;
    }

    private class PlayerImpl implements Player, Serializable {
        private static final long serialVersionUID = -5133543300206370649L;
        private PieceColor color;
        private PlayerObserver playerObserver;
        public PlayerImpl opposite;
        public PieceState king;
        public List<PieceState> pieces = new ArrayList<PieceState>();
        public String token;

        public PlayerImpl(PieceColor color) {
            this.color = color;
        }

        @Override
        public void move(BoardLocation source, BoardLocation destination,
                PieceType promotedType) {
            if (playerToMove() != color) {
                playerObserver.moveRejected(MoveRejectionReason.NOT_YOUR_TURN);
                return;
            }
            PieceState piece = cells[source.getRow()][source.getColumn()];
            PieceState capturedPiece = cells[destination.getRow()][destination.getColumn()];
            MoveResult moveResult = tryMove(source, destination, promotedType);
            if (moveResult.rejectionReason != null) {
                playerObserver.moveRejected(moveResult.rejectionReason);
                return;
            }
            MoveState state = MoveState.NONE;
            if (!opposite.canMove()) {
                if (opposite.isCheckState()) {
                    state = MoveState.CHECKMATE;
                } else {
                    state = MoveState.DRAW;
                }
            } else if (opposite.isCheckState()) {
                state = MoveState.CHECK;
            }
            Move move = new Move();
            move.setSource(source);
            move.setDestination(destination);
            move.setCapturedPiece(capturedPiece != null ? capturedPiece.getType() : null);
            move.setPiece(piece.getType());
            move.setState(state);
            moves.add(move);
            for (PlayerObserver observer : observers) {
                observer.moved(source, destination, promotedType, state);
            }
        }

        private MoveResult tryMove(final BoardLocation source, final BoardLocation destination,
                final PieceType promotedType) {
            MoveResult result = new MoveResult();
            result.rejectionReason = acceptMove(source, destination);
            if (result.rejectionReason != null) {
                return result;
            }
            final PieceState piece = cells[source.getRow()][source.getColumn()];
            final PieceType originalType = piece.getType();
            boolean promoted = false;
            if (piece.getType() == PieceType.PAWN) {
                if (piece.getColor() == PieceColor.WHITE && destination.getRow() == 7 ||
                        piece.getColor() == PieceColor.BLACK && destination.getRow() == 0) {
                    promoted = true;
                }
            }
            if (!promoted && promotedType != null) {
                result.rejectionReason = MoveRejectionReason.CANT_BE_PROMOTED;
                return result;
            }
            final PieceState capturedPiece = cells[destination.getRow()][destination.getColumn()];
            Runnable cancelAction = new Runnable() {
                @Override
                public void run() {
                    putPiece(source.getRow(), source.getColumn(), piece);
                    if (capturedPiece != null) {
                        putPiece(destination.getRow(), destination.getColumn(), capturedPiece);
                    } else {
                        cells[destination.getRow()][destination.getColumn()] = null;
                    }
                    piece.setType(originalType);
                }
            };
            if (capturedPiece != null) {
                capturedPiece.setLocation(null);
            }
            if (promotedType != null) {
                piece.setType(promotedType);
            }
            cells[source.getRow()][source.getColumn()] = null;
            putPiece(destination.getRow(), destination.getColumn(), piece);
            if (isCheckState()) {
                cancelAction.run();
                result.rejectionReason = MoveRejectionReason.CHECK;
                return result;
            }
            if (piece.getType() == PieceType.KING &&
                    Math.abs(source.getColumn() - destination.getColumn()) == 2) {
                final int rookColumn = destination.getColumn() > source.getColumn() ? 7 : 0;
                final PieceState rook = cells[source.getRow()][rookColumn];
                cells[source.getRow()][rookColumn] = null;
                putPiece(rook.getLocation().getRow(), rookColumn == 0 ? 3 : 5, rook);
                final Runnable innerCancelAction = cancelAction;
                cancelAction = new Runnable() {
                    @Override
                    public void run() {
                        putPiece(rook.getLocation().getRow(), rookColumn, rook);
                        innerCancelAction.run();
                    }
                };
            }
            result.cancel = cancelAction;
            return result;
        }

        private MoveRejectionReason acceptMove(BoardLocation source, BoardLocation destination) {
            if (source == null || destination == null || !source.isValid() ||
                    !destination.isValid()) {
                return MoveRejectionReason.INVALID_MOVE;
            }
            PieceState piece = cells[source.getRow()][source.getColumn()];
            if (piece == null) {
                return MoveRejectionReason.NO_PIECE_SELECTED;
            }
            if (piece.getColor() != color) {
                return MoveRejectionReason.NOT_YOUR_PIECE;
            }
            PieceState capturedPiece = cells[destination.getRow()][destination.getColumn()];
            if (capturedPiece != null) {
                if (capturedPiece.getColor() == piece.getColor()) {
                    return MoveRejectionReason.INVALID_MOVE;
                }
            }
            return acceptPieceMove(piece, destination) ? null : MoveRejectionReason.INVALID_MOVE;
        }

        private boolean acceptPieceMove(PieceState piece, BoardLocation destination) {
            switch (piece.getType()) {
                case PAWN:
                    return acceptPawnMove(piece, destination);
                case KNIGHT:
                    return acceptKnightMove(piece, destination);
                case BISHOP:
                    return acceptBishopMove(piece, destination);
                case ROOK:
                    return acceptRookMove(piece, destination);
                case QUEEN:
                    return acceptQueenMove(piece, destination);
                case KING:
                    return acceptKingMove(piece, destination);
                default:
                    return false;
            }
        }

        private boolean acceptPawnMove(PieceState pawn, BoardLocation destination) {
            // TODO: En passant
            BoardLocation source = pawn.getLocation();
            PieceState capturedPiece = cells[destination.getRow()][destination.getColumn()];
            int dir = pawn.getColor() == PieceColor.WHITE ? 1 : -1;
            if (capturedPiece == null) {
                if (destination.getColumn() != source.getColumn()) {
                    return false;
                }
                if (pawnMoved(pawn)) {
                    return source.getRow() + dir == destination.getRow();
                } else {
                    int dist = dir * (destination.getRow() - source.getRow());
                    if (dist > 2) {
                        return false;
                    }
                    if (dist == 2) {
                        PieceState tmpPiece = cells[source.getRow() + dir][source.getColumn()];
                        if (tmpPiece != null) {
                            return false;
                        }
                    }
                }
            } else {
                if (source.getRow() + dir != destination.getRow()) {
                    return false;
                }
                if (Math.abs(source.getColumn() - destination.getColumn()) != 1) {
                    return false;
                }
            }
            return true;
        }

        private boolean pawnMoved(PieceState pawn) {
            switch (pawn.getColor()) {
                case WHITE:
                    return pawn.getLocation().getRow() != 1;
                case BLACK:
                    return pawn.getLocation().getRow() != 6;
                default:
                    return true;
            }
        }

        private boolean acceptKnightMove(PieceState knight, BoardLocation destination) {
            BoardLocation source = knight.getLocation();
            int rowDelta = Math.abs(source.getRow() - destination.getRow());
            int columnDelta = Math.abs(source.getColumn() - destination.getColumn());
            return rowDelta == 1 && columnDelta == 2 || rowDelta == 2 && columnDelta == 1;
        }

        private boolean acceptBishopMove(PieceState bishop, BoardLocation destination) {
            BoardLocation source = bishop.getLocation();
            int rowDelta = Math.abs(source.getRow() - destination.getRow());
            int columnDelta = Math.abs(source.getColumn() - destination.getColumn());
            if (rowDelta != columnDelta) {
                return false;
            }
            int rowDir = source.getRow() < destination.getRow() ? 1 : -1;
            int columnDir = source.getColumn() < destination.getColumn() ? 1 : -1;
            int row = source.getRow();
            int column = source.getColumn();
            for (int i = 1; i < rowDelta; ++i) {
                 row += rowDir;
                 column += columnDir;
                 PieceState tmpPiece = cells[row][column];
                 if (tmpPiece != null) {
                     return false;
                 }
            }
            return true;
        }

        private boolean acceptRookMove(PieceState rook, BoardLocation destination) {
            BoardLocation source = rook.getLocation();
            int row = source.getRow();
            int column = source.getColumn();
            if (row == destination.getRow()) {
                int dir = column < destination.getColumn() ? 1 : -1;
                for (int i = column + dir; i != destination.getColumn(); i += dir) {
                    if (cells[row][i] != null) {
                        return false;
                    }
                }
                return true;
            } else if (column == destination.getColumn()) {
                int dir = row < destination.getRow() ? 1 : -1;
                for (int i = row + dir; i != destination.getRow(); i += dir) {
                    if (cells[i][column] != null) {
                        return false;
                    }
                }
                return true;
            } else {
                return false;
            }
        }

        private boolean acceptQueenMove(PieceState queen, BoardLocation destination) {
            return acceptBishopMove(queen, destination) || acceptRookMove(queen, destination);
        }

        private boolean acceptKingMove(PieceState king, BoardLocation destination) {
            BoardLocation source = king.getLocation();
            int rowDelta = Math.abs(source.getRow() - destination.getRow());
            int columnDelta = Math.abs(source.getColumn() - destination.getColumn());
            if (rowDelta <= 1 && columnDelta <= 1) {
                return true;
            } else if (columnDelta == 2 && rowDelta == 0) {
                int firstRow = color == PieceColor.WHITE ? 0 : 7;
                if (source.getRow() != firstRow) {
                    return false;
                }
                int dir = destination.getColumn() > source.getColumn() ? 1 : -1;
                BoardLocation rookLocation = new BoardLocation(firstRow,
                        destination.getColumn() > source.getColumn() ? 7 : 0);
                PieceState rook = cells[rookLocation.getRow()][rookLocation.getColumn()];
                if (rook == null || rook.getType() != PieceType.ROOK || rook.getColor() != color) {
                    return false;
                }
                for (int i = 0; i < moves.size(); ++i) {
                    PieceColor moveColor = i % 2 == 0 ? PieceColor.WHITE : PieceColor.BLACK;
                    if (moveColor != color) {
                        continue;
                    }
                    Move move = moves.get(i);
                    if (move.getSource().getRow() == rook.getLocation().getRow() &&
                            move.getSource().getRow() == rook.getLocation().getColumn() &&
                            move.getPiece() == PieceType.ROOK) {
                        return false;
                    }
                    if (move.getPiece() == PieceType.KING) {
                        return false;
                    }
                }
                BoardLocation tmpLoc = new BoardLocation(source.getRow(), source.getColumn());
                for (int i = 0; i < 2; ++i) {
                    for (PieceState piece : opposite.pieces) {
                        if (piece.getLocation() == null) {
                            continue;
                        }
                        if (acceptMove(piece.getLocation(), tmpLoc) == null) {
                            return false;
                        }
                        tmpLoc.setColumn(tmpLoc.getColumn() + dir);
                    }
                }
                tmpLoc.setColumn(source.getColumn() + dir);
                while (tmpLoc.getColumn() != rook.getLocation().getColumn()) {
                    if (cells[tmpLoc.getRow()][tmpLoc.getColumn()] != null) {
                        return false;
                    }
                    tmpLoc.setColumn(tmpLoc.getColumn() + dir);
                }
                return true;
            } else {
                return false;
            }
        }

        private boolean isCheckState() {
            for (PieceState piece : opposite.pieces) {
                if (piece.getLocation() == null) {
                    continue;
                }
                if (acceptPieceMove(piece, king.getLocation())) {
                    return true;
                }
            }
            return false;
        }

        private boolean canMove() {
            for (PieceState piece : pieces) {
                if (piece.getLocation() == null) {
                    continue;
                }
                for (BoardLocation target : getPieceMoves(piece)) {
                    MoveResult result = tryMove(piece.getLocation(), target, null);
                    if (result.cancel == null) {
                        continue;
                    }
                    boolean check = isCheckState();
                    result.cancel.run();
                    if (!check) {
                        return true;
                    }
                }
            }
            return false;
        }

        private List<BoardLocation> getPieceMoves(PieceState piece) {
            switch (piece.getType()) {
                case PAWN:
                    return getPawnMoves(piece);
                case KNIGHT:
                    return getKnightMoves(piece);
                case BISHOP:
                    return getBishopMoves(piece);
                case ROOK:
                    return getRookMoves(piece);
                case QUEEN:
                    return getQueenMoves(piece);
                case KING:
                    return getKingMoves(piece);
            }
            return Collections.emptyList();
        }

        private List<BoardLocation> getPawnMoves(PieceState pawn) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            BoardLocation location = pawn.getLocation();
            PieceColor color = pawn.getColor();
            int lastLine;
            int firstLine;
            int dir;
            switch (color) {
                case WHITE:
                    firstLine = 1;
                    lastLine = 7;
                    dir = 1;
                    break;
                case BLACK:
                    firstLine = 6;
                    lastLine = 0;
                    dir = -1;
                    break;
                default:
                    throw new RuntimeException();
            }
            if (location.getRow() != lastLine) {
                locations.add(new BoardLocation(location.getRow() + dir, location.getColumn()));
                if (location.getRow() == firstLine) {
                    locations.add(new BoardLocation(location.getRow() + dir,
                            location.getColumn() * 2));
                }
            }
            return locations;
        }

        private List<BoardLocation> getKnightMoves(PieceState knight) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            BoardLocation source = knight.getLocation();
            locations.add(new BoardLocation(source.getRow() + 1, source.getColumn() + 2));
            locations.add(new BoardLocation(source.getRow() + 2, source.getColumn() + 1));
            locations.add(new BoardLocation(source.getRow() + 2, source.getColumn() - 1));
            locations.add(new BoardLocation(source.getRow() + 1, source.getColumn() - 2));
            locations.add(new BoardLocation(source.getRow() - 1, source.getColumn() - 2));
            locations.add(new BoardLocation(source.getRow() - 2, source.getColumn() - 1));
            locations.add(new BoardLocation(source.getRow() - 2, source.getColumn() + 1));
            locations.add(new BoardLocation(source.getRow() - 1, source.getColumn() + 2));
            return locations;
        }

        private List<BoardLocation> getBishopMoves(PieceState bishop) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            locations.addAll(getDirectedMoves(bishop, 1, 1));
            locations.addAll(getDirectedMoves(bishop, -1, 1));
            locations.addAll(getDirectedMoves(bishop, -1, -1));
            locations.addAll(getDirectedMoves(bishop, 1, -1));
            return locations;
        }

        private List<BoardLocation> getRookMoves(PieceState rook) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            locations.addAll(getDirectedMoves(rook, 1, 0));
            locations.addAll(getDirectedMoves(rook, 0, 1));
            locations.addAll(getDirectedMoves(rook, -1, 0));
            locations.addAll(getDirectedMoves(rook, 0, -1));
            return locations;
        }

        private List<BoardLocation> getQueenMoves(PieceState queen) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            locations.addAll(getBishopMoves(queen));
            locations.addAll(getRookMoves(queen));
            return locations;
        }

        private List<BoardLocation> getDirectedMoves(PieceState piece, int horzDir, int vertDir) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            BoardLocation source = piece.getLocation();
            for (int i = 1; i < 8; ++i) {
                BoardLocation target = new BoardLocation(source.getRow() + vertDir * i,
                        source.getColumn() + horzDir * i);
                if (!target.isValid()) {
                    break;
                }
                PieceState targetPiece = cells[target.getRow()][target.getColumn()];
                if (targetPiece != null) {
                    if (targetPiece.getColor() != piece.getColor()) {
                        locations.add(target);
                    }
                    break;
                }
                locations.add(target);
            }
            return locations;
        }

        private List<BoardLocation> getKingMoves(PieceState king) {
            List<BoardLocation> locations = new ArrayList<BoardLocation>();
            BoardLocation location = king.getLocation();
            locations.add(new BoardLocation(location.getRow() + 0, location.getColumn() + 1));
            locations.add(new BoardLocation(location.getRow() + 1, location.getColumn() + 1));
            locations.add(new BoardLocation(location.getRow() + 1, location.getColumn() + 0));
            locations.add(new BoardLocation(location.getRow() + 1, location.getColumn() - 1));
            locations.add(new BoardLocation(location.getRow() + 0, location.getColumn() - 1));
            locations.add(new BoardLocation(location.getRow() - 1, location.getColumn() - 1));
            locations.add(new BoardLocation(location.getRow() - 1, location.getColumn() + 0));
            locations.add(new BoardLocation(location.getRow() - 1, location.getColumn() + 1));
            return locations;
        }

        public PlayerObserver getPlayerObserver() {
            return playerObserver;
        }

        public void setPlayerObserver(PlayerObserver playerObserver) {
            this.playerObserver = playerObserver;
        }
    }

    public DefaultBoard(String whiteToken, String blackToken) {
        for (int i = 0; i < cells.length; ++i) {
            cells[i] = new PieceState[8];
        }
        whitePlayer = new PlayerImpl(PieceColor.WHITE);
        whitePlayer.token = whiteToken;
        blackPlayer = new PlayerImpl(PieceColor.BLACK);
        blackPlayer.token = blackToken;
        players.put(PieceColor.WHITE, whitePlayer);
        players.put(PieceColor.BLACK, blackPlayer);
        whitePlayer.opposite = blackPlayer;
        blackPlayer.opposite = whitePlayer;
        createInitialPieces();
    }

    private void createInitialPieces() {
        List<PieceState> whitePieces = whitePlayer.pieces;
        for (int i = 0; i < 8; ++i) {
            whitePieces.add(putPiece(1, i, createWhitePiece(PieceType.PAWN)));
        }
        whitePieces.add(putPiece(0, 0, createWhitePiece(PieceType.ROOK)));
        whitePieces.add(putPiece(0, 7, createWhitePiece(PieceType.ROOK)));
        whitePieces.add(putPiece(0, 1, createWhitePiece(PieceType.KNIGHT)));
        whitePieces.add(putPiece(0, 6, createWhitePiece(PieceType.KNIGHT)));
        whitePieces.add(putPiece(0, 2, createWhitePiece(PieceType.BISHOP)));
        whitePieces.add(putPiece(0, 5, createWhitePiece(PieceType.BISHOP)));
        whitePieces.add(putPiece(0, 3, createWhitePiece(PieceType.QUEEN)));
        whitePlayer.king = createWhitePiece(PieceType.KING);
        whitePieces.add(putPiece(0, 4, whitePlayer.king));

        List<PieceState> blackPieces = blackPlayer.pieces;
        for (int i = 0; i < 8; ++i) {
            blackPieces.add(putPiece(6, i, createBlackPiece(PieceType.PAWN)));
        }
        blackPieces.add(putPiece(7, 7, createBlackPiece(PieceType.ROOK)));
        blackPieces.add(putPiece(7, 0, createBlackPiece(PieceType.ROOK)));
        blackPieces.add(putPiece(7, 6, createBlackPiece(PieceType.KNIGHT)));
        blackPieces.add(putPiece(7, 1, createBlackPiece(PieceType.KNIGHT)));
        blackPieces.add(putPiece(7, 5, createBlackPiece(PieceType.BISHOP)));
        blackPieces.add(putPiece(7, 2, createBlackPiece(PieceType.BISHOP)));
        blackPieces.add(putPiece(7, 3, createBlackPiece(PieceType.QUEEN)));
        blackPlayer.king = createBlackPiece(PieceType.KING);
        blackPieces.add(putPiece(7, 4, blackPlayer.king));
    }

    private PieceState createWhitePiece(PieceType type) {
        PieceState piece = new PieceState();
        piece.setColor(PieceColor.WHITE);
        piece.setType(type);
        return piece;
    }

    private PieceState createBlackPiece(PieceType type) {
        PieceState piece = new PieceState();
        piece.setColor(PieceColor.BLACK);
        piece.setType(type);
        return piece;
    }

    private PieceState putPiece(int row, int column, PieceState piece) {
        cells[row][column] = piece;
        piece.setLocation(new BoardLocation(row, column));
        return piece;
    }

    private PieceColor playerToMove() {
        return moves.size() % 2 == 0 ? PieceColor.WHITE : PieceColor.BLACK;
    }

    @Override
    public void authorizePlayer(PlayerObserver observer, String key) {
        boolean matches = false;
        for (PlayerImpl player : players.values()) {
            if (player.token.equals(key)) {
                observer.authorizationAccepted(player.color, player);
                sendFullState(observer);
                player.setPlayerObserver(observer);
                updateObserverList();
                matches = true;
                break;
            }
        }
        if (!matches) {
            observer.authorizationRejected();
        }
    }

    private void updateObserverList() {
        observers.clear();
        for (PlayerImpl player : players.values()) {
            if (player.getPlayerObserver() != null) {
                observers.add(player.getPlayerObserver());
            }
        }
    }

    private void sendFullState(PlayerObserver observer) {
        BoardState state = new BoardState();
        state.setMoves(moves);
        List<PieceState> pieces = new ArrayList<PieceState>();
        for (int i = 0; i < cells.length; ++i) {
            PieceState[] row = cells[i];
            for (int j = 0; j < row.length; ++j) {
                PieceState piece = row[j];
                if (piece != null) {
                    pieces.add(piece);
                }
            }
        }
        state.setPieces(pieces);
        observer.boardStateChanged(state);
    }
}
